Flier's Sky

天空,蓝色的天空,眼睛看不到的东西,眼睛看得到的东西

导航

用WinDbg探索CLR世界 [3] 跟踪方法的 JIT 过程

Posted on 2004-07-08 11:11  Flier Lu  阅读(2256)  评论(2编辑  收藏  举报
http://www.blogcn.com/user8/flier_lu/index.html?id=1678453&run=.05A880F

本来想按照 sos 的帮助文件上命令的分类逐步介绍 WinDbg 下使用 sos 调试 CLR 程序,但发现这样实在不够直观。索性改成根据我分析 CLR 的实际案例,step by step 介绍功能,这样结构上虽然混乱一点,但更加直观,也易于上手 :P

     前面两篇文章里面分别介绍了 WinDbg 的调试配置和线程的基本概念,这篇文章将针对 JIT 编译对象方法的流程进行分析,逐步介绍如何使用 WinDbg 调试 CLR 程序。

     用WinDbg探索CLR世界 [1] - 安装与环境配置
     用WinDbg探索CLR世界 [2] - 线程

     首先写一个简单的例子程序 demo.cs 并编译为 demo.exe,使用配置好的 WinDbg 打开之:
 

以下为引用:

 using System;

 namespace flier
 {
   class EntryPoint
   {
     public void m1()
     {
       System.Console.Write("EntryPoint.m1()");
     }

     public void m2()
     {
       System.Console.Write("EntryPoint.m2()");
     }

     public static void Main()
     {
       EntryPoint ep = new EntryPoint();

       ep.m1();
       ep.m2();
     }
   }
 }
 



     WinDbg 会在载入 demo.exe 后中断执行。此时可以使用 .load sos 命令加载 sos.dll 命令扩展,并用 .chain 验证加载是否成功;然后用 ld demo 命令加载 demo.exe 的调试符号文件,用 lm 命令验证加载是否成功。
     然后用 ld kernel32 加载 Kernel32 的调试符号文件,并用 bp kernel32!LoadLibraryExW "du poi(esp+4)" 命令在载入 DLL 的函数入口加上断点。接下来就是一路 g 指令,直到 mscorwks.dll 被加载。这个 mscorwks.dll 就是类似 JVM 中 jvm.dll 的虚拟机实现代码,我们要了解的大部分功能都在其中。详细的解释可以参看我以前的一篇文章《.Net平台下CLR程序载入原理分析》

     在 mscorwks.dll 被载入后用 ld mscorwks 命令载入其调试符号库,就可以正式开始我们的探索工作了 :D

     目前使用到的 WinDbg 命令如下

以下为引用:

 .load sos     // 加载 sos 调试扩展模块,可使用 .chain 命令验证

 ld demo       // 加载 demo.exe 调试符号库,可使用 lm 命令验证

 ld kernel32   // 加载 kernel32.exe 调试符号库

 bp kernel32!LoadLibraryExW "du poi(esp+4)" // 设置断点监视何时 mscorwks.dll 被载入

 g             // 执行直到 mscorwks.dll被加载

 bd 0          // 清除前面设置的断点,开始对 mscorwks.dll 进行处理

 ld mscorwks   // 加载 mscorwks.dll 调试符号库
 


     Don Box 在《.NET本质论 第1卷:公共语言运行库》的第六章介绍了方法调用的内部实现流程。其中提到方法表在 JIT 之前,保存的都是 call mscorwks.dll!PreStubWorker 调用,直到第一次使用时,才会对目标 IL 代码进行 JIT 编译,并调用之。因此我们第一步可以在此函数上设置断点(bp mscorwks!PreStubWorker),看看系统是如何调用此函数的。
 

以下为引用:

 0:000> bp mscorwks!PreStubWorker
 0:000> g
 ModLoad: 70ad0000 70bb6000   E:WINDOWSWinSxS†_Microsoft.Windows.Common-Controls_6595b64144ccf1df_6.0.100.0_x-ww_8417450Bcomctl32.dll
 ModLoad: 79780000 79980000   e:windowsmicrosoft.net rameworkv1.1.4322mscorlib.dll
 ModLoad: 79980000 79ca6000   e:windowsassembly ativeimages1_v1.1.4322mscorlib.0.5000.0__b77a5c561934e089_ed6bc96cmscorlib.dll
 ModLoad: 79510000 79523000   E:WINDOWSMicrosoft.NETFrameworkv1.1.4322mscorsn.dll
 Breakpoint 1 hit
 eax=0012f7c0 ebx=00148c60 ecx=04aa112c edx=00000004 esi=0012f784 edi=0012f9a8
 eip=791d6a4a esp=0012f764 ebp=0012f79c iopl=0         nv up ei pl zr na po nc
 cs=001b  ss=0023  ds=0023  es=0023  fs=0038  gs=0000             efl=00000246
 mscorwks!PreStubWorker:
 791d6a4a 55               push    ebp
 


     断点被激活就代表函数被调用。我们先使用 k 看看函数被调用时的上下文环境。
 
以下为引用:

 0:000> k
 ChildEBP RetAddr
 0012f760 0014930e mscorwks!PreStubWorker
 WARNING: Frame IP not in any known module. Following frames may be wrong.
 0012f79c 791da434 0x14930e
 0012f8b4 791dd2ec mscorwks!MethodDesc::CallDescr+0x1b6
 0012f96c 79240405 mscorwks!MethodDesc::Call+0xc5
 0012fa18 79240520 mscorwks!AppDomain::InitializeDomainContext+0x10f
 0012fa7c 7923d744 mscorwks!SystemDomain::InitializeDefaultDomain+0x11c
 0012fd60 791c6e73 mscorwks!SystemDomain::ExecuteMainMethod+0x120
 0012ffa0 791c6ef3 mscorwks!ExecuteEXE+0x1c0
 0012ffb0 7880a53e mscorwks!_CorExeMain+0x59
 0012ffc0 77e1f38c mscoree!_CorExeMain+0x30 [f:dd dpclrsrcdllsshimshim.cpp @ 5426]
 0012fff0 00000000 KERNEL32!BaseProcessStart+0x23
 


     这里可以看到从 mscoree!_CorExeMain 一路执行下来的步骤,而那个警告说明这个 stack frame 不在任意一个已知模块中。这是很正常的,因为这个栈帧实际上是指向由 JIT 动态生成的代码。我们监视的 mscorwks!PreStubWorker 函数只是作为方法表中函数的入口 stub,系统启动时还会通过其他方式调用 JIT 完成代码的编译执行。
     接下来用 SOS 的 !clrstack 命令看看 CLR 的调用堆栈,显示如下:
 
以下为引用:

 0:000> !clrstack
  succeeded
 Loaded Son of Strike data table version 5 from "E:WINDOWSMicrosoft.NETFrameworkv1.1.4322mscorwks.dll"
 Thread 0
 ESP       EIP
 0012f784  791d6a4a [FRAME: PrestubMethodFrame] [DEFAULT] [hasThis] Void System.AppDomain.SetupDomain(ValueClass System.LoaderOptimization,String,String)
 0012f9a8  791d6a4a [FRAME: GCFrame]
 0012fad0  791d6a4a [FRAME: DebuggerClassInitMarkFrame]
 0012fa94  791d6a4a [FRAME: GCFrame]
 


     如果需要更为详细的详细,可以使用 -p, -l 或 -r 参数分别显示参数、局部变量和寄存器,当然前两者需要调试符号库的支持才行。

     如此一路 g; !clrstack 执行下去,直到 flier.EntryPoint.m1 函数需要被处理为止:
 

以下为引用:

 0:000> !clrstack
 Thread 0
 ESP       EIP
 0012f68c  791d6a4a [FRAME: PrestubMethodFrame] [DEFAULT] [hasThis] Void flier.EntryPoint.m1()
 0012f69c  06d90080 [DEFAULT] Void flier.EntryPoint.Main()
 0012f9b0  791da717 [FRAME: GCFrame]
 0012fa94  791da717 [FRAME: GCFrame]
 


     此时用 !dumpstackobjects 命令可以查看当前线程堆栈中使用的所有对象
 
以下为引用:

 0:000> !dumpstackobjects
 ESP/REG  Object   Name
 ecx      04aa1a90 flier.EntryPoint
 0012f678 04aa1a90 flier.EntryPoint
 0012f67c 04aa1a90 flier.EntryPoint
 0012f680 04aa1a90 flier.EntryPoint
 


     这里的 flier.EntryPoint 对象地址 0x04aa1a90 就是我们要分析的对象在内存中的位置。

     这一阶段使用到的 WinDbg 命令如下:
 

以下为引用:

 bp mscorwks!PreStubWorker // 设置代码断点

 g                         // 继续运行至断点

 k                         // 查看函数调用时的 Native 堆栈调用

 !clrstack                 // 查看函数调用时的 CLR 堆栈调用

 !dumpstackobjects         // 查看线程堆栈中使用到的所有对象
 


     知道地址后,就可以用 !dumpobj 命令查看对象的详细信息
 

以下为引用:

 0:000> !dumpobj 04aa1a90
 Name: flier.EntryPoint
 MethodTable 0x009750a8
 EEClass 0x06c632e8
 Size 12(0xc) bytes
 mdToken: 02000002  (D:Tempdemo.exe)
 


     信息包括对象的类型名字(Name)和类型信息的地址(EEClass),以及对象的大小(Size)和 Token (mdToken),而方法表 (MethodTable) 正是我们分析方法调用的目标。我们可以用 !dumpclass 命令先进一步查看对象的类型信息:
 
以下为引用:

 0:000> !dumpclass 0x6c632e8
 Class Name : flier.EntryPoint
 mdToken : 02000002 ()
 Parent Class : 79b7c3c8
 ClassLoader : 00153850
 Method Table : 009750a8
 Vtable Slots : 4
 Total Method Slots : 8
 Class Attributes : 100000 :
 Flags : 1000003
 NumInstanceFields: 0
 NumStaticFields: 0
 ThreadStaticOffset: 0
 ThreadStaticsSize: 0
 ContextStaticOffset: 0
 ContextStaticsSize: 0
 


     可以发现其信息与对象信息有很多符合之处,正如 Don Box 所说,一个对象引用指向一个类型 EEClass 实例,而方法表为类型所有,其对象共有。我们可以使用 !dumpmt 命令进一步查看方法表的信息,-md 参数表示需要查看每个方法描述 (MethodDesc):
 
以下为引用:

 0:000> !dumpmt -md 0x09750a8
 EEClass : 06c632e8
 Module : 0014e090
 Name: flier.EntryPoint
 mdToken: 02000002  (D:Tempdemo.exe)
 MethodTable Flags : 80000
 Number of IFaces in IFaceMap : 0
 Interface Map : 009750f4
 Slots in VTable : 8
 --------------------------------------
 MethodDesc Table
   Entry  MethodDesc   JIT   Name
 79b7c4eb 79b7c4f0    None   [DEFAULT] [hasThis] String System.Object.ToString()
 79b7c473 79b7c478    None   [DEFAULT] [hasThis] Boolean System.Object.Equals(Object)
 79b7c48b 79b7c490    None   [DEFAULT] [hasThis] I4 System.Object.GetHashCode()
 79b7c52b 79b7c530    None   [DEFAULT] [hasThis] Void System.Object.Finalize()
 0097506b 00975070    None   [DEFAULT] [hasThis] Void flier.EntryPoint.m1()
 0097507b 00975080    None   [DEFAULT] [hasThis] Void flier.EntryPoint.m2()
 0097508b 00975090    None   [DEFAULT] Void flier.EntryPoint.Main()
 0097509b 009750a0    None   [DEFAULT] [hasThis] Void flier.EntryPoint..ctor()
 


     可以看到方法表中共有8个表项,其中前4个已经绑定到使用 ngen 预编译好的静态函数上
 
以下为引用:

 0:000> u 79b7c4eb
 mscorlib_79980000+0x1fc4eb:
 79b7c4eb e8909cfeff       call    mscorlib_79980000+0x1e6180 (79b66180)
 79b7c4f0 0000             add     [eax],al
 79b7c4f2 0080d86206c0     add     [eax+0xc00662d8],al
 79b7c4f8 06               push    es
 79b7c4f9 00fc             add     ah,bh
 79b7c4fb e8809cfeff       call    mscorlib_79980000+0x1e6180 (79b66180)
 79b7c500 07               pop     es
 79b7c501 0010             add     [eax],dl
 


     后四个则作为可被覆盖的虚方法在方法表中,这也是为什么在查看类型信息时 Vtable Slots = 4 而 Total Method Slots = 8 的原因。

     对方法表的每个项目,可以用 !DumpMD 命令查看详细描述,如
 

以下为引用:

 0:000> !DumpMD 0x00975070
 Method Name : [DEFAULT] [hasThis] Void flier.EntryPoint.m1()
 MethodTable 9750a8
 Module: 14e090
 mdToken: 06000001 (D:Tempdemo.exe)
 Flags : 0
 IL RVA : 00002050
 


     IL RVA 说明此方法的 IL 代码相对虚拟地址(IL RVA),也就是说此方法还没有被 JIT,仍以 IL 代码形式存在。对于已经完成 JIT 的方法,将显示其 JIT 后函数体代码的虚拟地址(Method VA):
 
以下为引用:

 0:000> !DumpMD 0x009750a0
 Method Name : [DEFAULT] [hasThis] Void flier.EntryPoint..ctor()
 MethodTable 9750a8
 Module: 14e090
 mdToken: 06000004 (D:Tempdemo.exe)
 Flags : 0
 Method VA : 06d900a8
 

     这一阶段使用到的 WinDbg 命令如下:
 

以下为引用:

 !dumpobj 04aa1a90     // 查看对象的详细信息

 !dumpclass 0x6c632e8  // 查看类型的详细信息

 !dumpmt -md 0x09750a8 // 查看方法表的详细信息

 !dumpmd 0x00975070    // 查看方法表项的方法描述的详细信息

 u 0x79b7c4eb          // 反汇编指定地址的指令
 


     我们反汇编一下 !DumpMT 命令列出的几个方法,就会发现正如 Don Box 所说,已经被 JIT 的代码指向一个jmp指令,直接跳转到编译后的方法体,如:
 

以下为引用:

 0:000> u 0097509b
 0097509b e908b04106       jmp     06d900a8
 


     而没有被 JIT 的函数,则指向一个call指令,调用一个 prolog 代码,间接调用 mscorwks!PreStubWorker 函数完成实际 JIT 工作,如:
 
以下为引用:

 0:000> u 0x0097506b
 0097506b e878427dff       call    001492e8

 0:000> u 0x0097507b
 0097507b e868427dff       call    001492e8
 



     这个 prolog 代码很简单,负责构造 mscorwks!PreStubWorker 所需的调用堆栈
 
以下为引用:

 0:000> u 0x001492e8
 001492e8 52               push    edx
 001492e9 68f0301b79       push    0x791b30f0
 001492ee 55               push    ebp
 001492ef 53               push    ebx
 001492f0 56               push    esi
 001492f1 57               push    edi
 001492f2 8d742410         lea     esi,[esp+0x10]
 001492f6 51               push    ecx
 001492f7 52               push    edx
 001492f8 648b1d2c0e0000   mov     ebx,fs:[00000e2c]
 001492ff 8b7b08           mov     edi,[ebx+0x8]
 00149302 897e04           mov     [esi+0x4],edi
 00149305 897308           mov     [ebx+0x8],esi
 00149308 56               push    esi
 00149309 e83cd70879       call    mscorwks!PreStubWorker (791d6a4a)
 0014930e 897b08           mov     [ebx+0x8],edi
 00149311 894604           mov     [esi+0x4],eax
 00149314 5a               pop     edx
 00149315 59               pop     ecx
 00149316 5f               pop     edi
 00149317 5e               pop     esi
 00149318 5b               pop     ebx
 00149319 5d               pop     ebp
 0014931a 83c404           add     esp,0x4
 0014931d 8f0424           pop     [esp]
 00149320 c3               ret
 


     而这段 prolog 代码是由类似 ROTOR 中的 GeneratePrestub 函数(vmi386cgenx86.cpp:1829) 动态生成的,完成对 PreStubWorker 函数调用的封装。而 PreStubWorker 函数会调用 JIT 完成真正的函数编译工作,并将方法表的入口改为指向编译后函数体的 jmp 指令。具体的流程请参考Don Box 在《.NET本质论 第1卷:公共语言运行库》的第六章中的介绍,这里就不再罗嗦了。以后有机会再写篇文章详细分析一下 JIT 的工作流程。

     在 JIT 处理 flier.EntryPoint.m1 时,用 g 命令执行,再回头来分析 m1 函数的入口,就会发现如前面所述,调用 JIT 过程的 call 指令变成了直接调用 Native 函数体的 jmp 指令。:D


     这一小节,我们介绍了使用 WinDbg 跟踪调试 CLR 程序的一遍流程,并了解了对堆栈、对象和类信息进行分析的 SOS 命令,希望大家能够借此开始探索 CLR 内部世界的旅程。 :P

     Jason Zander在其 BLog 的一篇文章,SOS Debugging with the CLR (Part 1),里面也详细介绍了使用 WinDbg 和 SOS 调试 CLR 程序的部分方法,值得一看。
     关于 WinDbg 的基本使用方法,CodeProject.com上有一个系列文章介绍
     
     Debug Tutorial Part 1: Beginning Debugging Using CDB and NTSD
     Debug Tutorial Part 2: The Stack
     Debug Tutorial Part 3: The Heap
     Debug Tutorial Part 4: Writing WINDBG Extensions